package com.noahsloan.nutils.bytes;

import java.io.IOException;
import java.io.InputStream;
import java.util.BitSet;

/**
 * Allows you to read 1 bit at a time from an InputStream.
 * 
 * Will buffer only 1 byte at a time. This means that reading a large number of
 * bytes at once will be fairly inefficient.
 * 
 * @author noah
 * 
 */
public class BitReader {
    private InputStream in; // input stream to read bits from

    private int buffer; // buffer to hold the current byte in

    private int bufferFill; // how much of the current byte have we already read

    /**
     * Creates a BitReader wrapping the given InputStream
     * 
     * @param in
     */
    public BitReader(InputStream in) {
        this.in = in;
        this.bufferFill = BitWriter.MAX_FILL + 1;
    }

    /**
     * Read 1 bit.
     * 
     * @return a bit
     * @throws IOException
     *             if the read operation on the InputStream throws an
     *             IOException
     * @throws UnexpectedEndOfStreamException
     *             if the buffer has been emptied and there are no more bytes to
     *             read from the underlying stream.
     */
    public boolean read() throws IOException, UnexpectedEndOfStreamException {
        if (bufferFill > BitWriter.MAX_FILL) {
            buffer = in.read();
            if (buffer == -1) {
                throw new UnexpectedEndOfStreamException(1);
            }
            bufferFill = 0;
        }
        int mask = (1 << BitWriter.MAX_FILL - bufferFill++);
        return (buffer & mask) != 0;
    }

    /**
     * Blocks until bits is filled.
     * 
     * @param bits
     *            the 'bit array' to fill.
     * @throws IOException
     *             if the read operation on the InputStream throws an
     *             IOException
     */
    public void read(boolean[] bits) throws IOException {
        for (int i = 0; i < bits.length; i++) {
            bits[i] = read();
        }
    }

    /**
     * Reads Short.SIZE bits and stuffs them into a short.
     * 
     * @return a short value.
     * @throws IOException
     *             if an IOError occurs.
     * @see java.lang.Short#SIZE
     */
    public short readShort() throws IOException {
        return (short) read(Short.SIZE);
    }

    /**
     * Reads Byte.SIZE bits and stuffs them into a byte.
     * 
     * @return a byte value.
     * @throws IOException
     * @see java.lang.Byte#SIZE
     */
    public byte readByte() throws IOException {
        return (byte) read(Byte.SIZE);
    }

    /**
     * Fills the given byte array with byte values from the stream.
     * 
     * @param buffer
     *            the buffer to fill.
     * @throws IOException
     *             if an IOError occurs.
     * @throws UnexpectedEndOfStreamException
     *             if there are not buffer.length bytes to be read
     */
    public void read(byte[] buffer) throws IOException {
        for (int i = 0; i < buffer.length; i++) {
            buffer[i] = readByte();
        }
    }

    /**
     * Reads Integer.SIZE bits and stuffs them into an int.
     * 
     * @return an int value.
     * @throws IOException
     *             if an IOError occurs.
     * @see java.lang.Integer#SIZE
     */
    public int readInt() throws IOException {
        return (int) read(Integer.SIZE);
    }

    /**
     * Reads Float.SIZE bits and stuffs them into a float.
     * 
     * @return a long value.
     * @throws IOException
     *             if an IOError occurs.
     * @see java.lang.Long#SIZE
     */
    public long readLong() throws IOException {
        return read(Long.SIZE);
    }

    /**
     * Reads Float.SIZE bits and stuffs them into a float.
     * 
     * @return a float value.
     * @throws IOException
     *             if an IOError occurs.
     * @see java.lang.Float#SIZE
     */
    public float readFloat() throws IOException {
        return Float.intBitsToFloat(readInt());
    }

    /**
     * Reads Double.SIZE bits and stuffs them into a double.
     * 
     * @return a double.
     * @throws IOException
     *             if an IOError occurs.
     * @see java.lang.Double#SIZE
     */
    public double readDouble() throws IOException {
        return Double.longBitsToDouble(readLong());
    }

    /**
     * Reads the next length bits into a bit set.
     * 
     * @param length
     * @return a new BitSet instance.
     * @throws IOException
     */
    public BitSet readBitSet(int length) throws IOException {
        BitSet bits = new BitSet(length);
        for (int i = 0; i < length; i++) {
            if (read()) {
                bits.set(i);
            }
        }
        return bits;
    }

    /**
     * Internal multi-bit reader. Reads the specified number of bits and stuffs
     * them into a long.
     * 
     * @param size
     * @return
     * @throws IOException
     *             if an IOError occurs, or if size is greater than Long.SIZE
     */
    protected long read(int size) throws IOException {
        long l = 0;
        if (size > Long.SIZE) {
            throw new IOException("Cannot fit " + size + " bits into a long!");
        }
        boolean[] bits = new boolean[size];
        read(bits);
        long b = 1l << (size - 1);
        for (boolean bit : bits) {
            if (bit) {
                l |= b;
            }
            b = b >>> 1;
        }
        return l;
    }

}
